大家好,我是 [您的名字/暱稱]。在前面的天數裡,我們探討了多執行緒 (Multi-threading) 的架構、優先級設計,以及如何優雅地清理那些失控的 Thread。今天,我們要將鏡頭拉近,來看一個在軟硬體整合時,幾乎所有工程師都曾踩過的地雷:time.sleep()。
在寫純軟體時,time.sleep() 是一個方便的工具;但在與硬體通訊(尤其是 Serial/UART、按鈕狀態、LED 控制)的世界裡,它卻可能是摧毀整個系統即時性的元兇。今天我們將以 DataDock 為例,探討為什麼我們必須戒掉 time.sleep(),以及如何利用非阻塞 (Non-blocking) 計時器與軟體防彈跳 (Debounce) 來打造一個反應靈敏的系統。
在硬體通訊中,我們經常遇到「需要等待硬體準備好」的情境。例如:
最直覺(也是最危險)的寫法就是:
# 絕對不要在主迴圈或通訊執行緒這樣寫
def wait_for_hardware():
serial.write(b'GET_DATA')
time.sleep(0.2) # 等待硬體回應
return serial.read_all()
time.sleep(0.2) 的期間,這條 Thread 什麼事都不能做。它不能接收中斷訊號,不能更新系統狀態,甚至無法提早結束程式(這也是為什麼我們在 Day 23 強調 Thread 難以優雅關閉的原因之一)。要取代 time.sleep(),我們必須擁抱狀態機 (State Machine) 與時間差計算。其核心思想是:「不要睡覺,而是不斷去問手錶現在幾點了」。
time.time() 進行非阻塞等待我們可以將原本死等的邏輯,改寫成這樣的非阻塞輪詢 (Polling):
import time
class HardwareCommunicator:
def __init__(self):
self.last_cmd_time = 0
self.timeout = 0.2
self.waiting_for_reply = False
def loop(self):
current_time = time.time()
# 如果正在等待硬體回應,檢查是否超時
if self.waiting_for_reply:
if current_time - self.last_cmd_time >= self.timeout:
print("硬體回應超時!")
self.waiting_for_reply = False
else:
# 檢查是否有資料進來,如果有就提早處理,不用傻等!
if serial.in_waiting > 0:
data = serial.read_all()
self.process_data(data)
self.waiting_for_reply = False
return # 本次迴圈結束,立刻把 CPU 讓給別人
# 若沒有在等待,則執行其他工作或發送下一個指令...
在 DataDock 的 sync_engine.py 與 led.py 中,我們大量使用了這種技巧。這確保了系統能夠在每一毫秒內迅速反應使用者的拔插動作 (Plug/Unplug),而不會因為某個 sleep 而卡在當下的狀態。
在拔插 USB (或按下硬體按鈕) 的瞬間,電氣訊號往往是不穩定的。在示波器下,訊號會在 High 和 Low 之間快速震盪(即 Bounce),這會導致作業系統或軟體誤以為設備被「快速插拔了無數次」。
這就是為什麼在前面的天數中,我們遇到了 "Software Re-plug" 的亂象,導致 Thread 瘋狂重生。為了解決這個問題,我們必須在軟體層導入 Debounce(防彈跳)機制。
Debounce 的目的是:只有當狀態維持穩定超過一段特定時間(例如 500 毫秒)後,我們才承認這個狀態的改變。
以下是我們在 DataDock 中實作 USB 連線狀態防彈跳的概念程式碼:
import time
class ConnectionMonitor:
def __init__(self):
self.current_state = False
self.last_stable_state = False
self.last_debounce_time = time.time()
self.debounce_delay = 0.5 # 500 毫秒防彈跳時間
def update_hardware_state(self, raw_state):
current_time = time.time()
# 如果硬體偵測到的原始狀態,跟我們目前紀錄的不一樣,代表訊號正在變動
if raw_state != self.current_state:
self.last_debounce_time = current_time
self.current_state = raw_state
# 如果狀態已經維持不變超過 debounce_delay,我們才正式採納它
if (current_time - self.last_debounce_time) > self.debounce_delay:
if self.current_state != self.last_stable_state:
self.last_stable_state = self.current_state
# 只有在這裡,才真正觸發「連線」或「斷線」的事件
if self.last_stable_state:
print("🚀 設備已穩固連接,啟動連線程序!")
else:
print("🛑 設備已徹底斷開,啟動清理程序!")
[!TIP]
軟硬體整合的黃金法則
將系統核心設計為事件驅動 (Event-driven) 或定時輪詢 (Polling) 的架構,不僅能有效解決 Bounce 造成的干擾,還能讓硬體故障或卡死時,軟體依然能維持基本運作,大幅提升系統的容錯率。
今天我們分享了在軟硬體通訊時,兩個非常實用的心法:
time.sleep():把時間控制權交還給主迴圈,系統反應速度會大幅提升。掌握了這兩個技巧,你的 Python 程式在面對實體世界的各種不穩定因素時,就能顯得游刃寫意。
明天(Day 25),我們將把這幾天學到的多執行緒、狀態機、非阻塞輪詢,全部收斂到一個終極設計模式:「有限狀態機 (FSM)」,來看看如何用更清晰的架構,掌控複雜的連線與同步邏輯!我們明天見!